N2O Bandit

TL;DR: Підтримка Elixir веб-сервера Bandit для N2O без модифікацій. Приклад використання.

Головна причина реврайтів — це пошук оптимальної реалізації. COWBOY веб сервер був довгий час головним і єдиним продакшин реді веб сервером для Erlang/OTP загалом і для Elixir DSL зокрема. Як на мене час компіляції COWLIB занадто великий і тільки це одне могло би бути причиною реврайта. Новий веб-сервер BANDIT цікавий тим, що дозволяє уніфікувати HTTP і WebSocket ендпойнти в інтерфейсах PLUG. Так як N2O може працювати поверх чистого веб-сокет сервера без HTTP ендпойнтів взагалі (їх можна сервити або з Github Pages або іншими веб-серверами, які відгружають статичні асети (JS, CSS, HTML), я використовува спочатку безпосередні біндінги THOUSAND_ISLAND (той самий автор, що і BANDIT), але зрештою вирішив закомітити уніфікований підхід до HTTP та WebSocket ендпойтів який пропонує PLUG.

N2O Static

COWBOY_STATIC модуль заміняється безпосередньо на Plug.Static. Але в продакшині ми взагалі можемо сервити сторінки без використання аплікейшин серверів.

defmodule Sample.Static do use Plug.Router plug Plug.Static, at: "/app", from: { :application.get_env(:n2o, :app, :sample), :application.get_env(:n2o, :upload, "priv/static") } match _ do send_resp(conn, 404, "Please refer to https://n2o.dev for more information.") end end

N2O WebSocket

Щоб завернути THOUSAND_ISLAND в PLUG потрібно використати допоміжну бібліотеку WEBSOCK_ADAPTER, з неї ми беремо тільки WebSocket Upgrade. Сам модуль є адаптером N2O для веб-сокет сервера BANDIT (під капотом THOUSAND_ISLAND).

defmodule Sample.WS do require N2O use Plug.Router plug :match plug :dispatch get "/ws/app/:mod", do: conn |> WebSockAdapter.upgrade(Sample.WS, [module: extract(mod)], timeout: 60_000) |> halt() def extract(route), do: :application.get_env(:n2o, :router, Sample.Application).route(route) def init(args), do: {:ok, N2O.cx(module: Keyword.get(args, :module)) } def handle_in({"N2O," <> _ = message, _}, state), do: response(:n2o_proto.stream({:text,message},[],state)) def handle_in({"PING", _}, state), do: {:reply, :ok, {:text, "PONG"}, state} def handle_in({message, _}, state) when is_binary(message), do: response(:n2o_proto.stream({:binary,message},[],state)) def handle_info(message, state), do: response(:n2o_proto.info(message,[],state)) def response({:reply,{:binary,rep},_,s}), do: {:reply,:ok,{:binary,rep},s} def response({:reply,{:text,rep},_,s}), do: {:reply,:ok,{:text,rep},s} def response({:reply,{:bert,rep},_,s}), do: {:reply,:ok,{:binary,:n2o_bert.encode(rep)},s} def response({:reply,{:json,rep},_,s}), do: {:reply,:ok,{:binary,:n2o_json.encode(rep)},s} match _ do send_resp(conn, 404, "Please refer to https://n2o.dev for more information.") end end

N2O Application

В головному файлі Erlang/OTP додатку стартуємо статичні і веб-сокет еднпойнти на різних портах. Це обмеження BANDIT відрізняється від старих способів деплоя N2O на одному порті, але ніби привносить більший порядочок в інфраструктуру.

defmodule Sample.Application do require N2O use Application # роутер для двох сторінок приклада ERPUNO/SAMPLE def route(<<"/ws/app/", p::binary>>), do: route(p) def route(<<"index", _::binary>>), do: Sample.Index def route(<<"login", _::binary>>), do: Sample.Login # інтерфейс N2O роутера def finish(state, ctx), do: {:ok, state, ctx} def init(state, context) do %{path: path} = N2O.cx(context, :req) {:ok, state, N2O.cx(context, path: path, module: route(path))} end # інтерфейс додатку Erlang/OTP def start(_, _) do :kvs.join() children = [ { Bandit, scheme: :http, port: 8002, plug: Sample.WS }, { Bandit, scheme: :http, port: 8004, plug: Sample.Static } ] Supervisor.start_link(children, strategy: :one_for_one, name: Sample.Supervisor) end end

N2O Deps

З залежностей N2O Bandit адаптер використовує тільки PLUG, BANDIT і WEBSOCK_ADAPTER. COWBOY — вже для історичної сумісності.

defmodule Sample.Mixfile do use Mix.Project def project() do [ app: :sample, version: "6.9.3", description: "SAMPLE Elixir N2O Application", deps: deps() ] end def deps() do [ {:plug, "~> 1.15.3"}, {:bandit, "~> 1.0"}, {:websock_adapter, "~> 0.5"}, {:rocksdb, "~> 1.8.0"}, {:nitro, "~> 8.2.4"}, {:kvs, "~> 10.8.3"}, {:n2o, "~> 10.12.4"}, {:syn, "~> 2.1.1"} ] end end

N2O Sample Login

Сторінки ERPUNO/SAMPLE не змінилися.

defmodule Sample.Login do require NITRO ; require Logger def event(:init) do :nitro.update(:loginButton, NITRO.button(id: :loginButton, body: "HELO", postback: :login, source: [:user, :room])) end def event(:login) do user = :nitro.to_list(:nitro.q(:user)) room = :nitro.to_binary(:nitro.q(:room)) :n2o.user(user) :n2o.session(:room, room) :nitro.wire("ws.close();") :nitro.redirect(["/app/index.htm?room=", room]) end def event(unexpected), do: unexpected |> inspect() |> Logger.warning() end

N2O Sample Index

defmodule Sample.Index do require NITRO ; require KVS ; require N2O ; require Logger def room() do case :n2o.session(:room) do '' -> "lobby" "" -> "lobby" x -> x end end def event(:init) do room = Sample.Index.room :kvs.ensure(KVS.writer(id: room)) ; :n2o.reg({:topic, room}) :nitro.update(:upload, NITRO.upload()) :nitro.update(:heading, NITRO.h2(id: :heading, body: room)) :nitro.update(:logout, NITRO.button(id: :logout, postback: :logout, body: "Logout")) :nitro.update(:send, NITRO.button(id: :send, body: "Chat", postback: :chat, source: [:message])) room |> :kvs.all() |> Enum.each(fn {:msg, _, user, message} -> event({:client, {user, message}}) end) end def event(:logout) do :n2o.user([]) :nitro.wire("ws.close();") :nitro.redirect("/app/login.htm") end def event(:chat), do: chat(:nitro.q(:message)) def event(N2O.ftp(sid: s, filename: f, status: {:event, :stop})) do name = hd(:lists.reverse(:string.tokens(:nitro.to_list(f), '/'))) link = NITRO.link(href: :erlang.iolist_to_binary(["/app/",s,"/",name]), body: name) chat(:nitro.render(link)) end def event({:client, {user, message}}) do :nitro.wire(NITRO.jq(target: :message, method: [:focus, :select])) :nitro.insert_top(:history, NITRO.message(body: [ NITRO.author(body: user), :nitro.jse(message) ])) end def event(unexpected), do: unexpected |> inspect() |> Logger.warning() def chat(message) do room = Sample.Index.room user = :n2o.user() room |> :kvs.writer() |> KVS.writer(args: {:msg, :kvs.seq([], []), user, message}) |> :kvs.add() |> :kvs.save() :n2o.send({:topic, room}, N2O.client(data: {user, message})) end end

N2O Sample Deploy

$ git clone https://github.com/erpuno/sample $ mix deps.get $ iex -S mix $ open http://localhost:8004/app/index.htm

˙

[1]. SAMPLE.ERP.UNO
[2]. GITHUB.COM/ERPUNO/SAMPLE